feat: Add support for using static discovery documents (#1109)

* feat: Add support for static discovery documents

* Auto generated docs should use static artifacts
diff --git a/describe.py b/describe.py
index 3d9d7a4..e53724e 100755
--- a/describe.py
+++ b/describe.py
@@ -37,6 +37,7 @@
 from googleapiclient.discovery import build
 from googleapiclient.discovery import build_from_document
 from googleapiclient.discovery import UnknownApiNameOrVersion
+from googleapiclient.discovery_cache import get_static_doc
 from googleapiclient.http import build_http
 from googleapiclient.errors import HttpError
 
@@ -395,6 +396,7 @@
   """
     try:
         service = build(name, version)
+        content = get_static_doc(name, version)
     except UnknownApiNameOrVersion as e:
         print("Warning: {} {} found but could not be built.".format(name, version))
         return
@@ -402,12 +404,6 @@
         print("Warning: {} {} returned {}.".format(name, version, e))
         return
 
-    http = build_http()
-    response, content = http.request(
-        uri or uritemplate.expand(
-            FLAGS.discovery_uri_template, {"api": name, "apiVersion": version}
-        )
-    )
     discovery = json.loads(content)
 
     version = safe_version(version)
diff --git a/docs/start.md b/docs/start.md
index e84db43..def0c4a 100644
--- a/docs/start.md
+++ b/docs/start.md
@@ -19,24 +19,24 @@
 These API calls do not access any private user data. Your application must authenticate itself as an application belonging to your Google Cloud project. This is needed to measure project usage for accounting purposes.
 
 **API key**: To authenticate your application, use an [API key](https://cloud.google.com/docs/authentication/api-keys) for your Google Cloud Console project. Every simple access call your application makes must include this key.
-    
+
 > **Warning**: Keep your API key private. If someone obtains your key, they could use it to consume your quota or incur charges against your Google Cloud project.
-    
+
 ### 2. Authorized API access (OAuth 2.0)
 
 These API calls access private user data. Before you can call them, the user that has access to the private data must grant your application access. Therefore, your application must be authenticated, the user must grant access for your application, and the user must be authenticated in order to grant that access. All of this is accomplished with [OAuth 2.0](https://developers.google.com/identity/protocols/OAuth2) and libraries written for it.
 
 *   **Scope**: Each API defines one or more scopes that declare a set of operations permitted. For example, an API might have read-only and read-write scopes. When your application requests access to user data, the request must include one or more scopes. The user needs to approve the scope of access your application is requesting. A list of accessible OAuth 2.0 scopes can be [found here](https://developers.google.com/identity/protocols/oauth2/scopes).
 *   **Refresh and access tokens**: When a user grants your application access, the OAuth 2.0 authorization server provides your application with refresh and access tokens. These tokens are only valid for the scope requested. Your application uses access tokens to authorize API calls. Access tokens expire, but refresh tokens do not. Your application can use a refresh token to acquire a new access token.
-    
+
     > **Warning**: Keep refresh and access tokens private. If someone obtains your tokens, they could use them to access private user data.
-    
+
 *   **Client ID and client secret**: These strings uniquely identify your application and are used to acquire tokens. They are created for your Google Cloud project on the [API Access pane](https://console.developers.google.com/apis/credentials) of the Google Cloud. There are several types of client IDs, so be sure to get the correct type for your application:
-    
+
     *   Web application client IDs
     *   Installed application client IDs
     *   [Service Account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount) client IDs
-    
+
     > **Warning**: Keep your client secret private. If someone obtains your client secret, they could use it to consume your quota, incur charges against your Google Cloud project, and request access to user data.
 
 ## Building and calling a service
@@ -45,7 +45,7 @@
 
 ### Build the service object
 
-Whether you are using simple or authorized API access, you use the [build()](http://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build) function to create a service object. It takes an API name and API version as arguments. You can see the list of all API versions on the [Supported APIs](dyn/index.md) page. The service object is constructed with methods specific to the given API. 
+Whether you are using simple or authorized API access, you use the [build()](http://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build) function to create a service object. It takes an API name and API version as arguments. You can see the list of all API versions on the [Supported APIs](dyn/index.md) page. When `build()` is called, a service object will attempt to be constructed with methods specific to the given API.
 
 `httplib2`, the underlying transport library, makes all connections persistent by default. Use the service object with a context manager or call `close` to avoid leaving sockets open.
 
@@ -65,6 +65,8 @@
     # ...
 ```
 
+**Note**: Under the hood, the `build()` function retrieves a discovery artifact in order to construct the service object.  If the `cache_discovery` argument of `build()` is set to `True`, the library will attempt to retrieve the discovery artifact from the legacy cache which is only supported with `oauth2client<4.0`. If the artifact is not available in the legacy cache and the `static_discovery` argument of `build()` is set to `True`, which is the default, the library will use the service definition shipped in the library. If always using the latest version of a service definition is more important than reliability, users should set `static_discovery=False` to retrieve the service definition from the internet.
+
 ### Collections
 
 Each API service provides access to one or more resources. A set of resources of the same type is called a collection. The names of these collections are specific to the API. The service object is constructed with a function for every collection defined by the API. If the given API has a collection named `stamps`, you create the collection object like this:
diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py
index b5d4696..44473dc 100644
--- a/googleapiclient/discovery.py
+++ b/googleapiclient/discovery.py
@@ -193,6 +193,7 @@
     adc_cert_path=None,
     adc_key_path=None,
     num_retries=1,
+    static_discovery=True,
 ):
     """Construct a Resource for interacting with an API.
 
@@ -246,6 +247,8 @@
       https://google.aip.dev/auth/4114
     num_retries: Integer, number of times to retry discovery with
       randomized exponential backoff in case of intermittent/connection issues.
+    static_discovery: Boolean, whether or not to use the static discovery docs
+      included in the library.
 
   Returns:
     A Resource object with methods for interacting with the service.
@@ -271,9 +274,12 @@
                 requested_url,
                 discovery_http,
                 cache_discovery,
+                serviceName,
+                version,
                 cache,
                 developerKey,
                 num_retries=num_retries,
+                static_discovery=static_discovery,
             )
             service = build_from_document(
                 content,
@@ -330,7 +336,15 @@
 
 
 def _retrieve_discovery_doc(
-    url, http, cache_discovery, cache=None, developerKey=None, num_retries=1
+    url,
+    http,
+    cache_discovery,
+    serviceName,
+    version,
+    cache=None,
+    developerKey=None,
+    num_retries=1,
+    static_discovery=True
 ):
     """Retrieves the discovery_doc from cache or the internet.
 
@@ -339,19 +353,23 @@
     http: httplib2.Http, An instance of httplib2.Http or something that acts
       like it through which HTTP requests will be made.
     cache_discovery: Boolean, whether or not to cache the discovery doc.
+    serviceName: string, name of the service.
+    version: string, the version of the service.
     cache: googleapiclient.discovery_cache.base.Cache, an optional cache
       object for the discovery documents.
     developerKey: string, Key for controlling API usage, generated
       from the API Console.
     num_retries: Integer, number of times to retry discovery with
       randomized exponential backoff in case of intermittent/connection issues.
+    static_discovery: Boolean, whether or not to use the static discovery docs
+      included in the library.
 
   Returns:
     A unicode string representation of the discovery document.
   """
-    if cache_discovery:
-        from . import discovery_cache
+    from . import discovery_cache
 
+    if cache_discovery:
         if cache is None:
             cache = discovery_cache.autodetect()
         if cache:
@@ -359,6 +377,15 @@
             if content:
                 return content
 
+    # When `static_discovery=True`, use static discovery artifacts included
+    # with the library
+    if static_discovery:
+        content = discovery_cache.get_static_doc(serviceName, version)
+        if content:
+            return content
+        else:
+            raise UnknownApiNameOrVersion("name: %s  version: %s" % (serviceName, version))
+
     actual_url = url
     # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
     # variable that contains the network address of the client sending the
diff --git a/googleapiclient/discovery_cache/__init__.py b/googleapiclient/discovery_cache/__init__.py
index 197f6bc..3f59e73 100644
--- a/googleapiclient/discovery_cache/__init__.py
+++ b/googleapiclient/discovery_cache/__init__.py
@@ -23,7 +23,8 @@
 LOGGER = logging.getLogger(__name__)
 
 DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24  # 1 day
-
+DISCOVERY_DOC_DIR = os.path.join(os.path.dirname(
+            os.path.realpath(__file__)), 'documents')
 
 def autodetect():
     """Detects an appropriate cache module and returns it.
@@ -48,3 +49,29 @@
         LOGGER.info("file_cache is only supported with oauth2client<4.0.0",
             exc_info=False)
         return None
+
+def get_static_doc(serviceName, version):
+    """Retrieves the discovery document from the directory defined in
+    DISCOVERY_DOC_DIR corresponding to the serviceName and version provided.
+
+    Args:
+        serviceName: string, name of the service.
+        version: string, the version of the service.
+
+    Returns:
+        A string containing the contents of the JSON discovery document,
+        otherwise None if the JSON discovery document was not found.
+    """
+
+    content = None
+    doc_name = "{}.{}.json".format(serviceName, version)
+
+    try:
+        with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), 'r') as f:
+            content = f.read()
+    except FileNotFoundError:
+        # File does not exist. Nothing to do here.
+        pass
+
+    return content
+
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index c6bc599..1a57ad3 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -447,7 +447,7 @@
 class DiscoveryErrors(unittest.TestCase):
     def test_tests_should_be_run_with_strict_positional_enforcement(self):
         try:
-            plus = build("plus", "v1", None)
+            plus = build("plus", "v1", None, static_discovery=False)
             self.fail("should have raised a TypeError exception over missing http=.")
         except TypeError:
             pass
@@ -455,7 +455,7 @@
     def test_failed_to_parse_discovery_json(self):
         self.http = HttpMock(datafile("malformed.json"), {"status": "200"})
         try:
-            plus = build("plus", "v1", http=self.http, cache_discovery=False)
+            plus = build("plus", "v1", http=self.http, cache_discovery=False, static_discovery=False)
             self.fail("should have raised an exception over malformed JSON.")
         except InvalidJsonError:
             pass
@@ -473,7 +473,7 @@
     def test_credentials_and_http_mutually_exclusive(self):
         http = HttpMock(datafile("plus.json"), {"status": "200"})
         with self.assertRaises(ValueError):
-            build("plus", "v1", http=http, credentials=mock.sentinel.credentials)
+            build("plus", "v1", http=http, credentials=mock.sentinel.credentials, static_discovery=False)
 
     def test_credentials_file_and_http_mutually_exclusive(self):
         http = HttpMock(datafile("plus.json"), {"status": "200"})
@@ -485,6 +485,7 @@
                 client_options=google.api_core.client_options.ClientOptions(
                     credentials_file="credentials.json"
                 ),
+                static_discovery=False,
             )
 
     def test_credentials_and_credentials_file_mutually_exclusive(self):
@@ -496,6 +497,7 @@
                 client_options=google.api_core.client_options.ClientOptions(
                     credentials_file="credentials.json"
                 ),
+                static_discovery=False,
             )
 
 
@@ -912,6 +914,7 @@
                 http=http,
                 developerKey=None,
                 discoveryServiceUrl="http://example.com",
+                static_discovery=False,
             )
             self.fail("Should have raised an exception.")
         except HttpError as e:
@@ -930,6 +933,7 @@
                 http=http,
                 developerKey=None,
                 discoveryServiceUrl="http://example.com",
+                static_discovery=False,
             )
             self.fail("Should have raised an exception.")
         except HttpError as e:
@@ -948,6 +952,7 @@
                 http=http,
                 developerKey="foo",
                 discoveryServiceUrl="http://example.com",
+                static_discovery=False,
             )
             self.fail("Should have raised an exception.")
         except HttpError as e:
@@ -960,7 +965,7 @@
                 ({"status": "200"}, read_datafile("zoo.json", "rb")),
             ]
         )
-        zoo = build("zoo", "v1", http=http, cache_discovery=False)
+        zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
         self.assertTrue(hasattr(zoo, "animals"))
 
     def test_api_endpoint_override_from_client_options(self):
@@ -975,7 +980,7 @@
             api_endpoint=api_endpoint
         )
         zoo = build(
-            "zoo", "v1", http=http, cache_discovery=False, client_options=options
+            "zoo", "v1", http=http, cache_discovery=False, client_options=options, static_discovery=False
         )
         self.assertEqual(zoo._baseUrl, api_endpoint)
 
@@ -993,12 +998,13 @@
             http=http,
             cache_discovery=False,
             client_options={"api_endpoint": api_endpoint},
+            static_discovery=False,
         )
         self.assertEqual(zoo._baseUrl, api_endpoint)
 
     def test_discovery_with_empty_version_uses_v2(self):
         http = HttpMockSequence([({"status": "200"}, read_datafile("zoo.json", "rb")),])
-        build("zoo", version=None, http=http, cache_discovery=False)
+        build("zoo", version=None, http=http, cache_discovery=False, static_discovery=False)
         validate_discovery_requests(self, http, "zoo", None, V2_DISCOVERY_URI)
 
     def test_discovery_with_empty_version_preserves_custom_uri(self):
@@ -1010,12 +1016,13 @@
             http=http,
             cache_discovery=False,
             discoveryServiceUrl=custom_discovery_uri,
+            static_discovery=False,
         )
         validate_discovery_requests(self, http, "zoo", None, custom_discovery_uri)
 
     def test_discovery_with_valid_version_uses_v1(self):
         http = HttpMockSequence([({"status": "200"}, read_datafile("zoo.json", "rb")),])
-        build("zoo", version="v123", http=http, cache_discovery=False)
+        build("zoo", version="v123", http=http, cache_discovery=False, static_discovery=False)
         validate_discovery_requests(self, http, "zoo", "v123", V1_DISCOVERY_URI)
 
 
@@ -1029,7 +1036,7 @@
         )
         with self.assertRaises(HttpError):
             with mock.patch("time.sleep") as mocked_sleep:
-                build("zoo", "v1", http=http, cache_discovery=False)
+                build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
 
         mocked_sleep.assert_called_once()
         # We also want to verify that we stayed with v1 discovery
@@ -1045,7 +1052,7 @@
         )
         with self.assertRaises(HttpError):
             with mock.patch("time.sleep") as mocked_sleep:
-                build("zoo", "v1", http=http, cache_discovery=False)
+                build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
 
         mocked_sleep.assert_called_once()
         # We also want to verify that we switched to v2 discovery
@@ -1059,7 +1066,7 @@
             ]
         )
         with mock.patch("time.sleep") as mocked_sleep:
-            zoo = build("zoo", "v1", http=http, cache_discovery=False)
+            zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
 
         self.assertTrue(hasattr(zoo, "animals"))
         mocked_sleep.assert_called_once()
@@ -1075,7 +1082,7 @@
             ]
         )
         with mock.patch("time.sleep") as mocked_sleep:
-            zoo = build("zoo", "v1", http=http, cache_discovery=False)
+            zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
 
         self.assertTrue(hasattr(zoo, "animals"))
         mocked_sleep.assert_called_once()
@@ -1111,7 +1118,7 @@
 
             self.mocked_api.memcache.get.return_value = None
 
-            plus = build("plus", "v1", http=self.http)
+            plus = build("plus", "v1", http=self.http, static_discovery=False)
 
             # memcache.get is called once
             url = "https://www.googleapis.com/discovery/v1/apis/plus/v1/rest"
@@ -1132,7 +1139,7 @@
             # (Otherwise it should through an error)
             self.http = HttpMock(None, {"status": "200"})
 
-            plus = build("plus", "v1", http=self.http)
+            plus = build("plus", "v1", http=self.http, static_discovery=False)
 
             # memcache.get is called twice
             self.mocked_api.memcache.get.assert_has_calls(
@@ -1148,6 +1155,26 @@
             )
 
 
+class DiscoveryFromStaticDocument(unittest.TestCase):
+    def test_retrieve_from_local_when_static_discovery_true(self):
+        http = HttpMockSequence([({"status": "400"}, "")])
+        drive = build("drive", "v3", http=http, cache_discovery=False,
+                          static_discovery=True)
+        self.assertIsNotNone(drive)
+        self.assertTrue(hasattr(drive, "files"))
+
+    def test_retrieve_from_internet_when_static_discovery_false(self):
+        http = HttpMockSequence([({"status": "400"}, "")])
+        with self.assertRaises(HttpError):
+            build("drive", "v3", http=http, cache_discovery=False,
+                      static_discovery=False)
+
+    def test_unknown_api_when_static_discovery_true(self):
+        with self.assertRaises(UnknownApiNameOrVersion):
+            build("doesnotexist", "v3", cache_discovery=False,
+                      static_discovery=True)
+
+
 class DictCache(Cache):
     def __init__(self):
         self.d = {}
@@ -1170,7 +1197,7 @@
         ):
             self.http = HttpMock(datafile("plus.json"), {"status": "200"})
 
-            plus = build("plus", "v1", http=self.http)
+            plus = build("plus", "v1", http=self.http, static_discovery=False)
 
             # cache.get is called once
             url = "https://www.googleapis.com/discovery/v1/apis/plus/v1/rest"
@@ -1187,7 +1214,7 @@
             # (Otherwise it should through an error)
             self.http = HttpMock(None, {"status": "200"})
 
-            plus = build("plus", "v1", http=self.http)
+            plus = build("plus", "v1", http=self.http, static_discovery=False)
 
             # cache.get is called twice
             cache.get.assert_has_calls([mock.call(url), mock.call(url)])
@@ -1199,7 +1226,7 @@
 class Discovery(unittest.TestCase):
     def test_method_error_checking(self):
         self.http = HttpMock(datafile("plus.json"), {"status": "200"})
-        plus = build("plus", "v1", http=self.http)
+        plus = build("plus", "v1", http=self.http, static_discovery=False)
 
         # Missing required parameters
         try:
@@ -1242,7 +1269,7 @@
 
     def test_type_coercion(self):
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
 
         request = zoo.query(
             q="foo", i=1.0, n=1.0, b=0, a=[1, 2, 3], o={"a": 1}, e="bar"
@@ -1275,7 +1302,7 @@
 
     def test_optional_stack_query_parameters(self):
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         request = zoo.query(trace="html", fields="description")
 
         parsed = urlparse(request.uri)
@@ -1285,7 +1312,7 @@
 
     def test_string_params_value_of_none_get_dropped(self):
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         request = zoo.query(trace=None, fields="description")
 
         parsed = urlparse(request.uri)
@@ -1294,7 +1321,7 @@
 
     def test_model_added_query_parameters(self):
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         request = zoo.animals().get(name="Lion")
 
         parsed = urlparse(request.uri)
@@ -1304,7 +1331,7 @@
 
     def test_fallback_to_raw_model(self):
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         request = zoo.animals().getmedia(name="Lion")
 
         parsed = urlparse(request.uri)
@@ -1314,7 +1341,7 @@
 
     def test_patch(self):
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         request = zoo.animals().patch(name="lion", body='{"description": "foo"}')
 
         self.assertEqual(request.method, "PATCH")
@@ -1322,7 +1349,7 @@
     def test_batch_request_from_discovery(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
         # zoo defines a batchPath
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
         batch_request = zoo.new_batch_http_request()
         self.assertEqual(
             batch_request._batch_uri, "https://www.googleapis.com/batchZoo"
@@ -1331,7 +1358,7 @@
     def test_batch_request_from_default(self):
         self.http = HttpMock(datafile("plus.json"), {"status": "200"})
         # plus does not define a batchPath
-        plus = build("plus", "v1", http=self.http, cache_discovery=False)
+        plus = build("plus", "v1", http=self.http, cache_discovery=False, static_discovery=False)
         batch_request = plus.new_batch_http_request()
         self.assertEqual(batch_request._batch_uri, "https://www.googleapis.com/batch")
 
@@ -1343,14 +1370,14 @@
             ]
         )
         http = tunnel_patch(http)
-        zoo = build("zoo", "v1", http=http, cache_discovery=False)
+        zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
         resp = zoo.animals().patch(name="lion", body='{"description": "foo"}').execute()
 
         self.assertTrue("x-http-method-override" in resp)
 
     def test_plus_resources(self):
         self.http = HttpMock(datafile("plus.json"), {"status": "200"})
-        plus = build("plus", "v1", http=self.http)
+        plus = build("plus", "v1", http=self.http, static_discovery=False)
         self.assertTrue(getattr(plus, "activities"))
         self.assertTrue(getattr(plus, "people"))
 
@@ -1381,7 +1408,7 @@
         # Zoo should exercise all discovery facets
         # and should also have no future.json file.
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
         self.assertTrue(getattr(zoo, "animals"))
 
         request = zoo.animals().list(name="bat", projection="full")
@@ -1392,7 +1419,7 @@
 
     def test_nested_resources(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
         self.assertTrue(getattr(zoo, "animals"))
         request = zoo.my().favorites().list(max_results="5")
         parsed = urlparse(request.uri)
@@ -1410,7 +1437,7 @@
 
     def test_top_level_functions(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
         self.assertTrue(getattr(zoo, "query"))
         request = zoo.query(q="foo")
         parsed = urlparse(request.uri)
@@ -1419,20 +1446,20 @@
 
     def test_simple_media_uploads(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
         doc = getattr(zoo.animals().insert, "__doc__")
         self.assertTrue("media_body" in doc)
 
     def test_simple_media_upload_no_max_size_provided(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
         request = zoo.animals().crossbreed(media_body=datafile("small.png"))
         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"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         try:
             zoo.animals().insert(media_body=datafile("smiley.png"))
@@ -1448,7 +1475,7 @@
 
     def test_simple_media_good_upload(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         request = zoo.animals().insert(media_body=datafile("small.png"))
         self.assertEqual("image/png", request.headers["content-type"])
@@ -1461,7 +1488,7 @@
 
     def test_simple_media_unknown_mimetype(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         try:
             zoo.animals().insert(media_body=datafile("small-png"))
@@ -1482,7 +1509,7 @@
 
     def test_multipart_media_raise_correct_exceptions(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         try:
             zoo.animals().insert(media_body=datafile("smiley.png"), body={})
@@ -1496,9 +1523,9 @@
         except UnacceptableMimeTypeError:
             pass
 
-    def test_multipart_media_good_upload(self):
+    def test_multipart_media_good_upload(self, static_discovery=False):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         request = zoo.animals().insert(media_body=datafile("small.png"), body={})
         self.assertTrue(request.headers["content-type"].startswith("multipart/related"))
@@ -1531,14 +1558,14 @@
 
     def test_media_capable_method_without_media(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         request = zoo.animals().insert(body={})
         self.assertTrue(request.headers["content-type"], "application/json")
 
     def test_resumable_multipart_media_good_upload(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body={})
@@ -1606,7 +1633,7 @@
     def test_resumable_media_good_upload(self):
         """Not a multipart upload."""
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body=None)
@@ -1665,7 +1692,7 @@
     def test_resumable_media_good_upload_from_execute(self):
         """Not a multipart upload."""
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body=None)
@@ -1704,7 +1731,7 @@
     def test_resumable_media_fail_unknown_response_code_first_request(self):
         """Not a multipart upload."""
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body=None)
@@ -1722,7 +1749,7 @@
     def test_resumable_media_fail_unknown_response_code_subsequent_request(self):
         """Not a multipart upload."""
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body=None)
@@ -1764,7 +1791,7 @@
 
     def test_media_io_base_stream_unlimited_chunksize_resume(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         # Set up a seekable stream and try to upload in single chunk.
         fd = BytesIO(b'01234"56789"')
@@ -1795,7 +1822,7 @@
 
     def test_media_io_base_stream_chunksize_resume(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         # Set up a seekable stream and try to upload in chunks.
         fd = BytesIO(b"0123456789")
@@ -1827,7 +1854,7 @@
         )
 
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         # Create an upload that doesn't know the full size of the media.
         class IoBaseUnknownLength(MediaUpload):
@@ -1854,7 +1881,7 @@
 
     def test_resumable_media_no_streaming_on_unsupported_platforms(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         class IoBaseHasStream(MediaUpload):
             def chunksize(self):
@@ -1907,7 +1934,7 @@
         )
 
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         fd = BytesIO(b"data goes here")
 
@@ -1931,7 +1958,7 @@
         )
 
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         # Create an upload that doesn't know the full size of the media.
         fd = BytesIO(b"data goes here")
@@ -1981,7 +2008,7 @@
         ]
 
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         self.assertEqual(sorted(zoo.__dict__.keys()), sorted_resource_keys)
 
         pickled_zoo = pickle.dumps(zoo)
@@ -2054,7 +2081,7 @@
         http = credentials.authorize(http)
         self.assertTrue(hasattr(http.request, "credentials"))
 
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         pickled_zoo = pickle.dumps(zoo)
         new_zoo = pickle.loads(pickled_zoo)
         self.assertEqual(sorted(zoo.__dict__.keys()), sorted(new_zoo.__dict__.keys()))
@@ -2063,7 +2090,7 @@
 
     def test_resumable_media_upload_no_content(self):
         self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=self.http)
+        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
 
         media_upload = MediaFileUpload(datafile("empty"), resumable=True)
         request = zoo.animals().insert(media_body=media_upload, body=None)
@@ -2134,7 +2161,7 @@
 
     def test_next_with_method_with_no_properties(self):
         self.http = HttpMock(datafile("latitude.json"), {"status": "200"})
-        service = build("latitude", "v1", http=self.http)
+        service = build("latitude", "v1", http=self.http, static_discovery=False)
         service.currentLocation().get()
 
     def test_next_nonexistent_with_no_next_page_token(self):
@@ -2156,7 +2183,7 @@
 class MediaGet(unittest.TestCase):
     def test_get_media(self):
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         request = zoo.animals().get_media(name="Lion")
 
         parsed = urlparse(request.uri)
diff --git a/tests/test_http.py b/tests/test_http.py
index 6292dc1..700a485 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -477,7 +477,7 @@
 class TestMediaIoBaseDownload(unittest.TestCase):
     def setUp(self):
         http = HttpMock(datafile("zoo.json"), {"status": "200"})
-        zoo = build("zoo", "v1", http=http)
+        zoo = build("zoo", "v1", http=http, static_discovery=False)
         self.request = zoo.animals().get_media(name="Lion")
         self.fd = BytesIO()
 
diff --git a/tests/test_mocks.py b/tests/test_mocks.py
index f020f6b..d0a0ff0 100644
--- a/tests/test_mocks.py
+++ b/tests/test_mocks.py
@@ -48,7 +48,7 @@
 
     def test_default_response(self):
         requestBuilder = RequestMockBuilder({})
-        plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder)
+        plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder, static_discovery=False)
         activity = plus.activities().get(activityId="tag:blah").execute()
         self.assertEqual({}, activity)
 
@@ -56,7 +56,7 @@
         requestBuilder = RequestMockBuilder(
             {"plus.activities.get": (None, '{"foo": "bar"}')}
         )
-        plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder)
+        plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder, static_discovery=False)
 
         activity = plus.activities().get(activityId="tag:blah").execute()
         self.assertEqual({"foo": "bar"}, activity)
@@ -64,7 +64,7 @@
     def test_unexpected_call(self):
         requestBuilder = RequestMockBuilder({}, check_unexpected=True)
 
-        plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder)
+        plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder, static_discovery=False)
 
         try:
             plus.activities().get(activityId="tag:blah").execute()
@@ -76,7 +76,7 @@
         requestBuilder = RequestMockBuilder(
             {"zoo.animals.insert": (None, '{"data": {"foo": "bar"}}', None)}
         )
-        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder)
+        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False)
 
         try:
             zoo.animals().insert(body="{}").execute()
@@ -88,7 +88,7 @@
         requestBuilder = RequestMockBuilder(
             {"zoo.animals.insert": (None, '{"data": {"foo": "bar"}}', "{}")}
         )
-        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder)
+        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False)
 
         try:
             zoo.animals().insert(body="").execute()
@@ -106,7 +106,7 @@
                 )
             }
         )
-        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder)
+        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False)
 
         try:
             zoo.animals().insert(body='{"data": {"foo": "blah"}}').execute()
@@ -124,7 +124,7 @@
                 )
             }
         )
-        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder)
+        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False)
 
         activity = zoo.animals().insert(body={"data": {"foo": "bar"}}).execute()
         self.assertEqual({"foo": "bar"}, activity)
@@ -139,7 +139,7 @@
                 )
             }
         )
-        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder)
+        zoo = build("zoo", "v1", http=self.zoo_http, requestBuilder=requestBuilder, static_discovery=False)
 
         activity = zoo.animals().insert(body={"data": {"foo": "bar"}}).execute()
         self.assertEqual({"foo": "bar"}, activity)
@@ -149,7 +149,7 @@
         requestBuilder = RequestMockBuilder(
             {"plus.activities.list": (errorResponse, b"{}")}
         )
-        plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder)
+        plus = build("plus", "v1", http=self.http, requestBuilder=requestBuilder, static_discovery=False)
 
         try:
             activity = (