feat: add client_options support for api endpoint override (#829)

* feat: add client_options support for api endpoint override
* chore: replace all `assertEquals` with `assertEqual`
diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py
index 87403b9..3158fb3 100644
--- a/googleapiclient/discovery.py
+++ b/googleapiclient/discovery.py
@@ -46,6 +46,7 @@
 # Third-party imports
 import httplib2
 import uritemplate
+import google.api_core.client_options
 
 # Local imports
 from googleapiclient import _auth
@@ -176,6 +177,7 @@
     credentials=None,
     cache_discovery=True,
     cache=None,
+    client_options=None,
 ):
     """Construct a Resource for interacting with an API.
 
@@ -202,6 +204,8 @@
     cache_discovery: Boolean, whether or not to cache the discovery doc.
     cache: googleapiclient.discovery_cache.base.CacheBase, an optional
       cache object for the discovery documents.
+    client_options: Dictionary or google.api_core.client_options, Client options to set user
+      options on the client. API endpoint should be set through client_options.
 
   Returns:
     A Resource object with methods for interacting with the service.
@@ -228,6 +232,7 @@
                 model=model,
                 requestBuilder=requestBuilder,
                 credentials=credentials,
+                client_options=client_options
             )
         except HttpError as e:
             if e.resp.status == http_client.NOT_FOUND:
@@ -304,6 +309,7 @@
     model=None,
     requestBuilder=HttpRequest,
     credentials=None,
+    client_options=None
 ):
     """Create a Resource for interacting with an API.
 
@@ -328,6 +334,8 @@
     credentials: oauth2client.Credentials or
       google.auth.credentials.Credentials, credentials to be used for
       authentication.
+    client_options: Dictionary or google.api_core.client_options, Client options to set user
+      options on the client. API endpoint should be set through client_options.
 
   Returns:
     A Resource object with methods for interacting with the service.
@@ -350,7 +358,16 @@
         )
         raise InvalidJsonError()
 
-    base = urljoin(service["rootUrl"], service["servicePath"])
+    # If an API Endpoint is provided on client options, use that as the base URL
+    base = urljoin(service['rootUrl'], service["servicePath"])
+    if client_options:
+        if type(client_options) == dict:
+            client_options = google.api_core.client_options.from_dict(
+                client_options
+            )
+        if client_options.api_endpoint:
+            base = client_options.api_endpoint
+
     schema = Schemas(service)
 
     # If the http client is not specified, then we must construct an http client
diff --git a/setup.py b/setup.py
index 617515b..82447e8 100644
--- a/setup.py
+++ b/setup.py
@@ -36,6 +36,7 @@
     "httplib2>=0.17.0,<1dev",
     "google-auth>=1.4.1",
     "google-auth-httplib2>=0.0.3",
+    "google-api-core>=1.13.0,<2dev",
     "six>=1.6.1,<2dev",
     "uritemplate>=3.0.0,<4dev",
 ]
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index f85035e..6400f21 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -466,7 +466,7 @@
         plus = build_from_document(
             discovery, base=base, credentials=self.MOCK_CREDENTIALS
         )
-        self.assertEquals("https://www.googleapis.com/plus/v1/", plus._baseUrl)
+        self.assertEqual("https://www.googleapis.com/plus/v1/", plus._baseUrl)
 
     def test_building_with_optional_http_with_authorization(self):
         discovery = open(datafile("plus.json")).read()
@@ -503,7 +503,7 @@
         plus = build_from_document(
             discovery, base="https://www.googleapis.com/", http=http
         )
-        self.assertEquals(plus._http, http)
+        self.assertEqual(plus._http, http)
 
     def test_building_with_developer_key_skips_adc(self):
         discovery = open(datafile("plus.json")).read()
@@ -515,6 +515,25 @@
         # application default credentials were used.
         self.assertNotIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp)
 
+    def test_api_endpoint_override_from_client_options(self):
+        discovery = open(datafile("plus.json")).read()
+        api_endpoint = "https://foo.googleapis.com/"
+        options = google.api_core.client_options.ClientOptions(
+            api_endpoint=api_endpoint
+        )
+        plus = build_from_document(discovery, client_options=options)
+
+        self.assertEqual(plus._baseUrl, api_endpoint)
+
+    def test_api_endpoint_override_from_client_options_dict(self):
+        discovery = open(datafile("plus.json")).read()
+        api_endpoint = "https://foo.googleapis.com/"
+        plus = build_from_document(
+            discovery, client_options={"api_endpoint": api_endpoint}
+        )
+
+        self.assertEqual(plus._baseUrl, api_endpoint)
+
 
 class DiscoveryFromHttp(unittest.TestCase):
     def setUp(self):
@@ -588,6 +607,39 @@
         zoo = build("zoo", "v1", http=http, cache_discovery=False)
         self.assertTrue(hasattr(zoo, "animals"))
 
+    def test_api_endpoint_override_from_client_options(self):
+        http = HttpMockSequence(
+            [
+                ({"status": "404"}, "Not found"),
+                ({"status": "200"}, open(datafile("zoo.json"), "rb").read()),
+            ]
+        )
+        api_endpoint = "https://foo.googleapis.com/"
+        options = google.api_core.client_options.ClientOptions(
+            api_endpoint=api_endpoint
+        )
+        zoo = build(
+            "zoo", "v1", http=http, cache_discovery=False, client_options=options
+        )
+        self.assertEqual(zoo._baseUrl, api_endpoint)
+
+    def test_api_endpoint_override_from_client_options_dict(self):
+        http = HttpMockSequence(
+            [
+                ({"status": "404"}, "Not found"),
+                ({"status": "200"}, open(datafile("zoo.json"), "rb").read()),
+            ]
+        )
+        api_endpoint = "https://foo.googleapis.com/"
+        zoo = build(
+            "zoo",
+            "v1",
+            http=http,
+            cache_discovery=False,
+            client_options={"api_endpoint": api_endpoint},
+        )
+        self.assertEqual(zoo._baseUrl, api_endpoint)
+
 
 class DiscoveryFromAppEngineCache(unittest.TestCase):
     def test_appengine_memcache(self):
@@ -928,8 +980,8 @@
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
         zoo = build("zoo", "v1", http=self.http)
         request = zoo.animals().crossbreed(media_body=datafile("small.png"))
-        self.assertEquals("image/png", request.headers["content-type"])
-        self.assertEquals(b"PNG", request.body[1:4])
+        self.assertEqual("image/png", request.headers["content-type"])
+        self.assertEqual(b"PNG", request.body[1:4])
 
     def test_simple_media_raise_correct_exceptions(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
@@ -952,8 +1004,8 @@
         zoo = build("zoo", "v1", http=self.http)
 
         request = zoo.animals().insert(media_body=datafile("small.png"))
-        self.assertEquals("image/png", request.headers["content-type"])
-        self.assertEquals(b"PNG", request.body[1:4])
+        self.assertEqual("image/png", request.headers["content-type"])
+        self.assertEqual(b"PNG", request.body[1:4])
         assertUrisEqual(
             self,
             "https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json",
@@ -973,8 +1025,8 @@
         request = zoo.animals().insert(
             media_body=datafile("small-png"), media_mime_type="image/png"
         )
-        self.assertEquals("image/png", request.headers["content-type"])
-        self.assertEquals(b"PNG", request.body[1:4])
+        self.assertEqual("image/png", request.headers["content-type"])
+        self.assertEqual(b"PNG", request.body[1:4])
         assertUrisEqual(
             self,
             "https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json",
@@ -1045,13 +1097,13 @@
         media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body={})
         self.assertTrue(request.headers["content-type"].startswith("application/json"))
-        self.assertEquals('{"data": {}}', request.body)
-        self.assertEquals(media_upload, request.resumable)
+        self.assertEqual('{"data": {}}', request.body)
+        self.assertEqual(media_upload, request.resumable)
 
-        self.assertEquals("image/png", request.resumable.mimetype())
+        self.assertEqual("image/png", request.resumable.mimetype())
 
         self.assertNotEquals(request.body, None)
-        self.assertEquals(request.resumable_uri, None)
+        self.assertEqual(request.resumable_uri, None)
 
         http = HttpMockSequence(
             [
@@ -1078,32 +1130,32 @@
         )
 
         status, body = request.next_chunk(http=http)
-        self.assertEquals(None, body)
+        self.assertEqual(None, body)
         self.assertTrue(isinstance(status, MediaUploadProgress))
-        self.assertEquals(0, status.resumable_progress)
+        self.assertEqual(0, status.resumable_progress)
 
         # Two requests should have been made and the resumable_uri should have been
         # updated for each one.
-        self.assertEquals(request.resumable_uri, "http://upload.example.com/2")
-        self.assertEquals(media_upload, request.resumable)
-        self.assertEquals(0, request.resumable_progress)
+        self.assertEqual(request.resumable_uri, "http://upload.example.com/2")
+        self.assertEqual(media_upload, request.resumable)
+        self.assertEqual(0, request.resumable_progress)
 
         # This next chuck call should upload the first chunk
         status, body = request.next_chunk(http=http)
-        self.assertEquals(request.resumable_uri, "http://upload.example.com/3")
-        self.assertEquals(media_upload, request.resumable)
-        self.assertEquals(13, request.resumable_progress)
+        self.assertEqual(request.resumable_uri, "http://upload.example.com/3")
+        self.assertEqual(media_upload, request.resumable)
+        self.assertEqual(13, request.resumable_progress)
 
         # This call will upload the next chunk
         status, body = request.next_chunk(http=http)
-        self.assertEquals(request.resumable_uri, "http://upload.example.com/4")
-        self.assertEquals(media_upload.size() - 1, request.resumable_progress)
-        self.assertEquals('{"data": {}}', request.body)
+        self.assertEqual(request.resumable_uri, "http://upload.example.com/4")
+        self.assertEqual(media_upload.size() - 1, request.resumable_progress)
+        self.assertEqual('{"data": {}}', request.body)
 
         # Final call to next_chunk should complete the upload.
         status, body = request.next_chunk(http=http)
-        self.assertEquals(body, {"foo": "bar"})
-        self.assertEquals(status, None)
+        self.assertEqual(body, {"foo": "bar"})
+        self.assertEqual(status, None)
 
     def test_resumable_media_good_upload(self):
         """Not a multipart upload."""
@@ -1112,12 +1164,12 @@
 
         media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body=None)
-        self.assertEquals(media_upload, request.resumable)
+        self.assertEqual(media_upload, request.resumable)
 
-        self.assertEquals("image/png", request.resumable.mimetype())
+        self.assertEqual("image/png", request.resumable.mimetype())
 
-        self.assertEquals(request.body, None)
-        self.assertEquals(request.resumable_uri, None)
+        self.assertEqual(request.body, None)
+        self.assertEqual(request.resumable_uri, None)
 
         http = HttpMockSequence(
             [
@@ -1143,26 +1195,26 @@
         )
 
         status, body = request.next_chunk(http=http)
-        self.assertEquals(None, body)
+        self.assertEqual(None, body)
         self.assertTrue(isinstance(status, MediaUploadProgress))
-        self.assertEquals(13, status.resumable_progress)
+        self.assertEqual(13, status.resumable_progress)
 
         # Two requests should have been made and the resumable_uri should have been
         # updated for each one.
-        self.assertEquals(request.resumable_uri, "http://upload.example.com/2")
+        self.assertEqual(request.resumable_uri, "http://upload.example.com/2")
 
-        self.assertEquals(media_upload, request.resumable)
-        self.assertEquals(13, request.resumable_progress)
+        self.assertEqual(media_upload, request.resumable)
+        self.assertEqual(13, request.resumable_progress)
 
         status, body = request.next_chunk(http=http)
-        self.assertEquals(request.resumable_uri, "http://upload.example.com/3")
-        self.assertEquals(media_upload.size() - 1, request.resumable_progress)
-        self.assertEquals(request.body, None)
+        self.assertEqual(request.resumable_uri, "http://upload.example.com/3")
+        self.assertEqual(media_upload.size() - 1, request.resumable_progress)
+        self.assertEqual(request.body, None)
 
         # Final call to next_chunk should complete the upload.
         status, body = request.next_chunk(http=http)
-        self.assertEquals(body, {"foo": "bar"})
-        self.assertEquals(status, None)
+        self.assertEqual(body, {"foo": "bar"})
+        self.assertEqual(status, None)
 
     def test_resumable_media_good_upload_from_execute(self):
         """Not a multipart upload."""
@@ -1201,7 +1253,7 @@
         )
 
         body = request.execute(http=http)
-        self.assertEquals(body, {"foo": "bar"})
+        self.assertEqual(body, {"foo": "bar"})
 
     def test_resumable_media_fail_unknown_response_code_first_request(self):
         """Not a multipart upload."""
@@ -1247,7 +1299,7 @@
         )
 
         status, body = request.next_chunk(http=http)
-        self.assertEquals(
+        self.assertEqual(
             status.resumable_progress,
             7,
             "Should have first checked length and then tried to PUT more.",
@@ -1571,9 +1623,9 @@
         media_upload = MediaFileUpload(datafile("empty"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body=None)
 
-        self.assertEquals(media_upload, request.resumable)
-        self.assertEquals(request.body, None)
-        self.assertEquals(request.resumable_uri, None)
+        self.assertEqual(media_upload, request.resumable)
+        self.assertEqual(request.body, None)
+        self.assertEqual(request.resumable_uri, None)
 
         http = HttpMockSequence(
             [
@@ -1590,9 +1642,9 @@
         )
 
         status, body = request.next_chunk(http=http)
-        self.assertEquals(None, body)
+        self.assertEqual(None, body)
         self.assertTrue(isinstance(status, MediaUploadProgress))
-        self.assertEquals(0, status.progress())
+        self.assertEqual(0, status.progress())
 
 
 class Next(unittest.TestCase):
diff --git a/tests/test_http.py b/tests/test_http.py
index 2bf5060..ce27e2e 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -1122,7 +1122,7 @@
 
     def test_id_to_from_content_id_header(self):
         batch = BatchHttpRequest()
-        self.assertEquals("12", batch._header_to_id(batch._id_to_header("12")))
+        self.assertEqual("12", batch._header_to_id(batch._id_to_header("12")))
 
     def test_invalid_content_id_header(self):
         batch = BatchHttpRequest()
@@ -1646,7 +1646,7 @@
     def test_build_http_default_timeout_can_be_set_to_zero(self):
         socket.setdefaulttimeout(0)
         http = build_http()
-        self.assertEquals(http.timeout, 0)
+        self.assertEqual(http.timeout, 0)
     
     def test_build_http_default_308_is_excluded_as_redirect(self):
         http = build_http()