diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70032ef..bc1d040 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@
 
 [1]: https://pypi.org/project/google-auth/#history
 
+### [1.10.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.10.1...v1.10.2) (2020-01-18)
+
+
+### Bug Fixes
+
+* make collections import compatible across Python versions ([#419](https://www.github.com/googleapis/google-auth-library-python/issues/419)) ([c5a3395](https://www.github.com/googleapis/google-auth-library-python/commit/c5a3395b8781e14c4566cf0e476b234d6a1c1224)), closes [#418](https://www.github.com/googleapis/google-auth-library-python/issues/418)
+
 ### [1.10.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.10.0...v1.10.1) (2020-01-10)
 
 
diff --git a/google/auth/jwt.py b/google/auth/jwt.py
index a30c575..06e7679 100644
--- a/google/auth/jwt.py
+++ b/google/auth/jwt.py
@@ -40,7 +40,11 @@ You can also skip verification::
 
 """
 
-import collections
+try:
+    from collections.abc import Mapping
+# Python 2.7 compatibility
+except ImportError:  # pragma: NO COVER
+    from collections import Mapping
 import copy
 import datetime
 import json
@@ -215,7 +219,7 @@ def decode(token, certs=None, verify=True, audience=None):
 
     # If certs is specified as a dictionary of key IDs to certificates, then
     # use the certificate identified by the key ID in the token header.
-    if isinstance(certs, collections.Mapping):
+    if isinstance(certs, Mapping):
         key_id = header.get("kid")
         if key_id:
             if key_id not in certs:
diff --git a/google/auth/transport/requests.py b/google/auth/transport/requests.py
index 1caec0b..ce78e63 100644
--- a/google/auth/transport/requests.py
+++ b/google/auth/transport/requests.py
@@ -239,18 +239,38 @@ class AuthorizedSession(requests.Session):
         # credentials.refresh).
         self._auth_request = auth_request
 
-    def request(self, method, url, data=None, headers=None, timeout=None, **kwargs):
+    def request(
+        self,
+        method,
+        url,
+        data=None,
+        headers=None,
+        max_allowed_time=None,
+        timeout=None,
+        **kwargs
+    ):
         """Implementation of Requests' request.
 
         Args:
-            timeout (Optional[Union[float, Tuple[float, float]]]): The number
-                of seconds to wait before raising a ``Timeout`` exception. If
-                multiple requests are made under the hood, ``timeout`` is
-                interpreted as the approximate total time of **all** requests.
-
-                If passed as a tuple ``(connect_timeout, read_timeout)``, the
-                smaller of the values is taken as the total allowed time across
-                all requests.
+            timeout (Optional[Union[float, Tuple[float, float]]]):
+                The amount of time in seconds to wait for the server response
+                with each individual request.
+
+                Can also be passed as a tuple (connect_timeout, read_timeout).
+                See :meth:`requests.Session.request` documentation for details.
+
+            max_allowed_time (Optional[float]):
+                If the method runs longer than this, a ``Timeout`` exception is
+                automatically raised. Unlike the ``timeout` parameter, this
+                value applies to the total method execution time, even if
+                multiple requests are made under the hood.
+
+                Mind that it is not guaranteed that the timeout error is raised
+                at ``max_allowed_time`. It might take longer, for example, if
+                an underlying request takes a lot of time, but the request
+                itself does not timeout, e.g. if a large file is being
+                transmitted. The timout error will be raised after such
+                request completes.
         """
         # pylint: disable=arguments-differ
         # Requests has a ton of arguments to request, but only two
@@ -273,11 +293,13 @@ class AuthorizedSession(requests.Session):
             else functools.partial(self._auth_request, timeout=timeout)
         )
 
-        with TimeoutGuard(timeout) as guard:
+        remaining_time = max_allowed_time
+
+        with TimeoutGuard(remaining_time) as guard:
             self.credentials.before_request(auth_request, method, url, request_headers)
-        timeout = guard.remaining_timeout
+        remaining_time = guard.remaining_timeout
 
-        with TimeoutGuard(timeout) as guard:
+        with TimeoutGuard(remaining_time) as guard:
             response = super(AuthorizedSession, self).request(
                 method,
                 url,
@@ -286,7 +308,7 @@ class AuthorizedSession(requests.Session):
                 timeout=timeout,
                 **kwargs
             )
-        timeout = guard.remaining_timeout
+        remaining_time = guard.remaining_timeout
 
         # If the response indicated that the credentials needed to be
         # refreshed, then refresh the credentials and re-attempt the
@@ -305,14 +327,6 @@ class AuthorizedSession(requests.Session):
                 self._max_refresh_attempts,
             )
 
-            if self._refresh_timeout is not None:
-                if timeout is None:
-                    timeout = self._refresh_timeout
-                elif isinstance(timeout, numbers.Number):
-                    timeout = min(timeout, self._refresh_timeout)
-                else:
-                    timeout = tuple(min(x, self._refresh_timeout) for x in timeout)
-
             # Do not apply the timeout unconditionally in order to not override the
             # _auth_request's default timeout.
             auth_request = (
@@ -321,17 +335,18 @@ class AuthorizedSession(requests.Session):
                 else functools.partial(self._auth_request, timeout=timeout)
             )
 
-            with TimeoutGuard(timeout) as guard:
+            with TimeoutGuard(remaining_time) as guard:
                 self.credentials.refresh(auth_request)
-            timeout = guard.remaining_timeout
+            remaining_time = guard.remaining_timeout
 
             # Recurse. Pass in the original headers, not our modified set, but
-            # do pass the adjusted timeout (i.e. the remaining time).
+            # do pass the adjusted max allowed time (i.e. the remaining total time).
             return self.request(
                 method,
                 url,
                 data=data,
                 headers=headers,
+                max_allowed_time=remaining_time,
                 timeout=timeout,
                 _credential_refresh_attempt=_credential_refresh_attempt + 1,
                 **kwargs
diff --git a/setup.py b/setup.py
index 1b5eac4..708a5d1 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@ DEPENDENCIES = (
 with io.open("README.rst", "r") as fh:
     long_description = fh.read()
 
-version = "1.10.1"
+version = "1.10.2"
 
 setup(
     name="google-auth",

Make a clear distinction between the transport timeout and the total method time timeout
It turned out that the concern from #406 was valid, unfortunately:

> A downstream library that is timeout-sensitive might still be affected by the change, because the new explicit timeout parameter changes the semantics of AuthorizedSession.request() . The timeout represents the total allowed time for all requests made behind the scenes (e.g. credentials refresh attempts), as opposed to a previous per-request timeout.

The changed semantics of `timeout` caused an [issue](https://github.com/googleapis/google-cloud-python/issues/10137) in the `storage` library. A user reported that if a large file upload takes, say, minutes _and succeeds_, a ``Timeout`` error is still raised, because the total execution time takes more than ~60 seconds, which the default transport timeout configured here in this library here.

### Proposed solution
Leave the `timeout` parameter as is and still pass it to the transport, but introduce and expose a new optional parameter `max_allowed_time` that can be used instead to configure the "total timeout".

> NOTE: The method itself might still take longer than `max_allowed_time`, because the underlying request might take longer, which is the nature of the `requests` library's timeout ([docs](https://2.python-requests.org/en/master/user/quickstart/#timeouts)).

### Alternatives
Modifying the library code so that it supports "absolute total timeouts", but that would require interrupting requests in-flight, which does not seem to be a straightforward task -https://stackoverflow.com/q/21965484/5040035

diff --git a/tests/transport/test_requests.py b/tests/transport/test_requests.py
index 0026974..8f73d4b 100644
--- a/tests/transport/test_requests.py
+++ b/tests/transport/test_requests.py
@@ -220,7 +220,25 @@ class TestAuthorizedHttp(object):
         assert adapter.requests[1].url == self.TEST_URL
         assert adapter.requests[1].headers["authorization"] == "token1"
 
-    def test_request_timeout(self, frozen_time):
+    def test_request_max_allowed_time_timeout_error(self, frozen_time):
+        tick_one_second = functools.partial(frozen_time.tick, delta=1.0)
+
+        credentials = mock.Mock(
+            wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+        )
+        adapter = TimeTickAdapterStub(
+            time_tick=tick_one_second, responses=[make_response(status=http_client.OK)]
+        )
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+        authed_session.mount(self.TEST_URL, adapter)
+
+        # Because a request takes a full mocked second, max_allowed_time shorter
+        # than that will cause a timeout error.
+        with pytest.raises(requests.exceptions.Timeout):
+            authed_session.request("GET", self.TEST_URL, max_allowed_time=0.9)
+
+    def test_request_max_allowed_time_w_transport_timeout_no_error(self, frozen_time):
         tick_one_second = functools.partial(frozen_time.tick, delta=1.0)
 
         credentials = mock.Mock(
@@ -237,12 +255,12 @@ class TestAuthorizedHttp(object):
         authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
         authed_session.mount(self.TEST_URL, adapter)
 
-        # Because at least two requests have to be made, and each takes one
-        # second, the total timeout specified will be exceeded.
-        with pytest.raises(requests.exceptions.Timeout):
-            authed_session.request("GET", self.TEST_URL, timeout=1.9)
+        # A short configured transport timeout does not affect max_allowed_time.
+        # The latter is not adjusted to it and is only concerned with the actual
+        # execution time. The call below should thus not raise a timeout error.
+        authed_session.request("GET", self.TEST_URL, timeout=0.5, max_allowed_time=3.1)
 
-    def test_request_timeout_w_refresh_timeout(self, frozen_time):
+    def test_request_max_allowed_time_w_refresh_timeout_no_error(self, frozen_time):
         tick_one_second = functools.partial(frozen_time.tick, delta=1.0)
 
         credentials = mock.Mock(
@@ -257,15 +275,17 @@ class TestAuthorizedHttp(object):
         )
 
         authed_session = google.auth.transport.requests.AuthorizedSession(
-            credentials, refresh_timeout=1.9
+            credentials, refresh_timeout=1.1
         )
         authed_session.mount(self.TEST_URL, adapter)
 
-        # The timeout is long, but the short refresh timeout will prevail.
-        with pytest.raises(requests.exceptions.Timeout):
-            authed_session.request("GET", self.TEST_URL, timeout=60)
+        # A short configured refresh timeout does not affect max_allowed_time.
+        # The latter is not adjusted to it and is only concerned with the actual
+        # execution time. The call below should thus not raise a timeout error
+        # (and `timeout` does not come into play either, as it's very long).
+        authed_session.request("GET", self.TEST_URL, timeout=60, max_allowed_time=3.1)
 
-    def test_request_timeout_w_refresh_timeout_and_tuple_timeout(self, frozen_time):
+    def test_request_timeout_w_refresh_timeout_timeout_error(self, frozen_time):
         tick_one_second = functools.partial(frozen_time.tick, delta=1.0)
 
         credentials = mock.Mock(
@@ -284,7 +304,10 @@ class TestAuthorizedHttp(object):
         )
         authed_session.mount(self.TEST_URL, adapter)
 
-        # The shortest timeout will prevail and cause a Timeout error, despite
-        # other timeouts being quite long.
+        # An UNAUTHORIZED response triggers a refresh (an extra request), thus
+        # the final request that otherwise succeeds results in a timeout error
+        # (all three requests together last 3 mocked seconds).
         with pytest.raises(requests.exceptions.Timeout):
-            authed_session.request("GET", self.TEST_URL, timeout=(100, 2.9))
+            authed_session.request(
+                "GET", self.TEST_URL, timeout=60, max_allowed_time=2.9
+            )

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-localserver requests urllib3 cryptography grpcio mock freezegun flask oauth2client
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
blinker==1.9.0
cachetools==4.2.4
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
coverage==7.8.0
cryptography==44.0.2
exceptiongroup==1.2.2
Flask==3.1.0
freezegun==1.5.1
-e git+https://github.com/googleapis/google-auth-library-python.git@f07fbe9995bc55d15c37bcf5fa1d00f90c846cf3#egg=google_auth
grpcio==1.71.0
httplib2==0.22.0
idna==3.10
importlib_metadata==8.6.1
iniconfig==2.1.0
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
mock==5.2.0
oauth2client==4.1.3
packaging==24.2
pluggy==1.5.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.22
pyparsing==3.2.3
pytest==8.3.5
pytest-cov==6.0.0
pytest-localserver==0.9.0.post0
python-dateutil==2.9.0.post0
requests==2.32.3
rsa==4.0
six==1.17.0
tomli==2.2.1
urllib3==2.3.0
Werkzeug==3.1.3
zipp==3.21.0

name: google-auth-library-python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - blinker==1.9.0
      - cachetools==4.2.4
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - click==8.1.8
      - coverage==7.8.0
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - flask==3.1.0
      - freezegun==1.5.1
      - grpcio==1.71.0
      - httplib2==0.22.0
      - idna==3.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - itsdangerous==2.2.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mock==5.2.0
      - oauth2client==4.1.3
      - packaging==24.2
      - pluggy==1.5.0
      - pyasn1==0.6.1
      - pyasn1-modules==0.4.2
      - pycparser==2.22
      - pyparsing==3.2.3
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytest-localserver==0.9.0.post0
      - python-dateutil==2.9.0.post0
      - requests==2.32.3
      - rsa==4.0
      - six==1.17.0
      - tomli==2.2.1
      - urllib3==2.3.0
      - werkzeug==3.1.3
      - zipp==3.21.0
prefix: /opt/conda/envs/google-auth-library-python

tests/transport/test_requests.py::TestAuthorizedHttp::test_request_max_allowed_time_timeout_error tests/transport/test_requests.py::TestAuthorizedHttp::test_request_max_allowed_time_w_transport_timeout_no_error tests/transport/test_requests.py::TestAuthorizedHttp::test_request_max_allowed_time_w_refresh_timeout_no_error tests/transport/test_requests.py::TestAuthorizedHttp::test_request_timeout_w_refresh_timeout_timeout_error
tests/transport/test_requests.py::TestRequestResponse::test_request_basic tests/transport/test_requests.py::TestRequestResponse::test_request_with_timeout_success tests/transport/test_requests.py::TestRequestResponse::test_request_with_timeout_failure tests/transport/test_requests.py::TestRequestResponse::test_request_headers tests/transport/test_requests.py::TestRequestResponse::test_request_error tests/transport/test_requests.py::TestRequestResponse::test_connection_error tests/transport/test_requests.py::TestRequestResponse::test_timeout tests/transport/test_requests.py::TestTimeoutGuard::test_tracks_elapsed_time_w_numeric_timeout tests/transport/test_requests.py::TestTimeoutGuard::test_tracks_elapsed_time_w_tuple_timeout tests/transport/test_requests.py::TestTimeoutGuard::test_noop_if_no_timeout tests/transport/test_requests.py::TestTimeoutGuard::test_timeout_error_w_numeric_timeout tests/transport/test_requests.py::TestTimeoutGuard::test_timeout_error_w_tuple_timeout tests/transport/test_requests.py::TestTimeoutGuard::test_custom_timeout_error_type tests/transport/test_requests.py::TestTimeoutGuard::test_lets_suite_errors_bubble_up tests/transport/test_requests.py::TestAuthorizedHttp::test_constructor tests/transport/test_requests.py::TestAuthorizedHttp::test_constructor_with_auth_request tests/transport/test_requests.py::TestAuthorizedHttp::test_request_no_refresh tests/transport/test_requests.py::TestAuthorizedHttp::test_request_refresh
setup.py google/auth/transport/requests.py CHANGELOG.md google/auth/jwt.py
setup.py google/auth/transport/requests.py CHANGELOG.md google/auth/jwt.py